package nr.co.mhgames.polyanim.util;

import java.util.Locale;

/**
 * Basic 4x4 matrix class. These matrices are used for computing transformations
 * for renderers that don't implement transformations themselves. The matrix
 * operations are also needed when anchors are used.
 * 
 * @author Mika Halttunen
 */
public class Matrix {
	/** Matrix contents */
	protected float[][] m;

	/** Identity matrix as an array */
	protected static final float identity[][] = new float[][] { { 1, 0, 0, 0 }, { 0, 1, 0, 0 },
			{ 0, 0, 1, 0 }, { 0, 0, 0, 1 } };

	/**
	 * Default constructor. This does <b>not</b> initialize an identity matrix,
	 * but a zero matrix!
	 */
	public Matrix() {
		m = new float[4][4];
	}

	/** Constructor from a 2d float array */
	public Matrix(float mat[][]) {
		m = copyMat(m, mat);
	}

	/** Constructor from a 16-element float array */
	public Matrix(float mat[]) {
		this();
		m[0][0] = mat[0];
		m[0][1] = mat[1];
		m[0][2] = mat[2];
		m[0][3] = mat[3];
		m[1][0] = mat[4];
		m[1][1] = mat[5];
		m[1][2] = mat[6];
		m[1][3] = mat[7];
		m[2][0] = mat[8];
		m[2][1] = mat[9];
		m[2][2] = mat[10];
		m[2][3] = mat[11];
		m[3][0] = mat[12];
		m[3][1] = mat[13];
		m[3][2] = mat[14];
		m[3][3] = mat[15];
	}

	/** Constructor from another matrix */
	public Matrix(Matrix mat) {
		m = copyMat(m, mat.m);
	}


	/** Load the identity matrix */
	public void loadIdentity() {
		copyMat(m, identity);
	}

	/**
	 * Return the matrix contents as a 4x4 float array.
	 * 
	 * @param mat
	 *            Array of floats (can be <code>null</code>)
	 * @return The new array (4x4)
	 */
	public float[][] getM(float mat[][]) {
		return copyMat(mat, m);
	}

	/**
	 * Return the matrix contents as a 16 element array.
	 * 
	 * @param mat
	 *            Array of floats (can be <code>null</code>)
	 * @return The new array (16 elements)
	 */
	public float[] getM(float mat[]) {
		if (mat == null) {
			mat = new float[16];
		}
		System.arraycopy(m[0], 0, mat, 0, 4);
		System.arraycopy(m[1], 0, mat, 4, 4);
		System.arraycopy(m[2], 0, mat, 8, 4);
		System.arraycopy(m[3], 0, mat, 12, 4);

		return mat;
	}


	/** Set the translation in XY plane. */
	public void setTranslation(float x, float y) {
		m[3][0] = x;
		m[3][1] = y;
	}

	/** Translate the matrix. Multiplies the matrix with the given translation. */
	public void translate(float x, float y) {
		float t[][] = newIdentity();
		t[3][0] = x;
		t[3][1] = y;

		// Apply the transformation
		multiply(t);
	}

	/** Set the rotation (degrees) */
	void setRotationDegree(float deg) {
		double ang = deg * Math.PI / 180.0;
		m[0][0] = (float) Math.cos(ang);
		m[1][0] = (float) -Math.sin(ang);
		m[0][1] = -m[1][0];
		m[1][1] = m[0][0];
	}

	/**
	 * Rotate the matrix by given degrees. Multiplies the matrix with the
	 * rotation.
	 */
	public void rotateDegree(float deg) {
		double ang = deg * Math.PI / 180.0;
		float t[][] = newIdentity();
		t[0][0] = (float) Math.cos(ang);
		t[1][0] = (float) -Math.sin(ang);
		t[0][1] = -t[1][0];
		t[1][1] = t[0][0];

		// Apply the transformation
		multiply(t);
	}

	/** Set the rotation (radians) */
	void setRotationRadian(float rad) {
		double ang = rad;
		m[0][0] = (float) Math.cos(ang);
		m[1][0] = (float) -Math.sin(ang);
		m[0][1] = -m[1][0];
		m[1][1] = m[0][0];
	}

	/**
	 * Rotate the matrix by given radians. Multiplies the matrix with the
	 * rotation.
	 */
	public void rotateRadian(float rad) {
		double ang = rad;
		float t[][] = newIdentity();
		t[0][0] = (float) Math.cos(ang);
		t[1][0] = (float) -Math.sin(ang);
		t[0][1] = -t[1][0];
		t[1][1] = t[0][0];

		// Apply the transformation
		multiply(t);
	}

	/** Set the scale */
	void setScale(float x, float y) {
		m[0][0] = x;
		m[1][0] = 0;
		m[0][1] = 0;
		m[1][1] = y;
	}

	/**
	 * Scale the matrix by given factors. Multiplies the matrix with the scaling
	 * factors.
	 */
	public void scale(float x, float y) {
		float t[][] = newIdentity();
		t[0][0] = x;
		// t[1][0] = 0; // These two are not needed, because t is an identity
		// t[0][1] = 0;
		t[1][1] = y;

		// Apply the transformation
		multiply(t);
	}


	/**
	 * Multiply the matrix by another matrix.
	 * 
	 * @param mat
	 *            Matrix to multiply with
	 */
	public void multiply(Matrix mat) {
		Matrix mul = new Matrix();
		for (int r = 0; r < 4; r++) {
			for (int c = 0; c < 4; c++) {
				mul.m[r][c] = mat.m[r][0] * m[0][c] + mat.m[r][1] * m[1][c] + mat.m[r][2] * m[2][c]
						+ mat.m[r][3] * m[3][c];
			}
		}

		copyMat(m, mul.m);
	}

	/**
	 * Multiply the matrix by given float array (4x4).
	 * 
	 * @param mat
	 *            4x4 float array
	 */
	public void multiply(float mat[][]) {
		float mul[][] = new float[4][4];
		copyMat(mul, m);

		for (int r = 0; r < 4; r++) {
			for (int c = 0; c < 4; c++) {
				mul[r][c] = mat[r][0] * m[0][c] + mat[r][1] * m[1][c] + mat[r][2] * m[2][c]
						+ mat[r][3] * m[3][c];
			}
		}

		copyMat(m, mul);
	}


	/**
	 * Transforms the given point by the matrix.
	 * 
	 * @param point
	 *            Point to transform
	 * @return Transformed point
	 */
	public Vector2f transformPoint(Vector2f point) {
		float x = m[0][0] * point.x + m[1][0] * point.y + m[3][0];
		float y = m[0][1] * point.x + m[1][1] * point.y + m[3][1];
		return new Vector2f(x, y);
	}


	/**
	 * Helper method that returns a new float array (4x4) that contains the
	 * identity matrix.
	 * 
	 * @return Identity matrix in 4x4 array
	 */
	private static float[][] newIdentity() {
		float t[][] = new float[4][4];
		copyMat(t, identity);
		return t;
	}

	/**
	 * Helper method that copies a matrix array to another. It's assumed that
	 * both matrices are 4x4. Destination can be <code>null</code> - in that
	 * case 4x4 array is created and returned.
	 * 
	 * @param dst
	 *            Destination array (4x4), can be <code>null</code>.
	 * @param src
	 *            Source array (4x4)
	 * @return The new array (4x4)
	 */
	private static float[][] copyMat(float dst[][], float src[][]) {
		if (dst == null) {
			dst = new float[4][4];
		}
		System.arraycopy(src[0], 0, dst[0], 0, 4);
		System.arraycopy(src[1], 0, dst[1], 0, 4);
		System.arraycopy(src[2], 0, dst[2], 0, 4);
		System.arraycopy(src[3], 0, dst[3], 0, 4);

		return dst;
	}


	/**
	 * Return the matrix as a string.
	 */
	@Override
	public String toString() {
		StringBuilder buf = new StringBuilder("Matrix:\n");

		for (int c = 0; c < 4; c++) {
			for (int r = 0; r < 4; r++) {
				buf.append(String.format((Locale) null, "%3.2f, ", m[r][c]));
			}

			buf.append('\n');
		}
		return buf.toString();
	}


	public static void main(String args[]) {
		// Matrix decomposition test
		// ////////////////////////////////////////////////////////

		Matrix mat1 = new Matrix(identity);

		mat1.translate(100, 50);
		System.out.println(mat1);

		mat1.rotateDegree(45);
		System.out.println(mat1);

		mat1.scale(0.45f, 0.8f);
		System.out.println(mat1);

		double sx = mat1.m[1][0] * mat1.m[1][0] + mat1.m[1][1] * mat1.m[1][1];
		sx = Math.sqrt(sx);

		double sy = mat1.m[0][0] * mat1.m[0][0] + mat1.m[0][1] * mat1.m[0][1];
		sy = Math.sqrt(sy);

		Matrix rot = new Matrix(identity);
		rot.m[0][0] = (float) (mat1.m[0][0] / sy);
		rot.m[1][0] = (float) (mat1.m[1][0] / sx);
		rot.m[0][1] = (float) (mat1.m[0][1] / sy);
		rot.m[1][1] = (float) (mat1.m[1][1] / sx);

		System.out.println(rot);


		double x = mat1.m[3][0];
		double y = mat1.m[3][1];
		System.out.println("tx: " + x);
		System.out.println("ty: " + y);

		sx = mat1.m[0][0] / rot.m[0][0];
		sy = mat1.m[1][1] / rot.m[1][1];

		x = sx;
		y = sy;
		System.out.println("sx: " + x);
		System.out.println("sy: " + y);

		double angle = Math.acos(rot.m[0][0]);
		System.out.println("ang: " + Math.toDegrees(angle));
	}
}
